(function () {
    'use strict';
    var describe = BDD.describe, it = BDD.it, expect = BDD.expect;

    function permutedTripleEquals(a, b, j, eql, tolerance) {
        var i;

        for (i = 0; i < 3; i += 1) {
            if (!eql(a[i], b[(i + j) % 3], eql, tolerance)) {
                return false;
            }
        }

        return true;
    }

    function orderedTriple(a, b, eql, tolerance) {
        var i;

        if (a.length !== 3 || b.length !== 3) {
            return false;
        }

        for (i = 0; i < 3; i += 1) {
            if (permutedTripleEquals(a, b, i, eql, tolerance)) {
                return true;
            }
        }

        return false;
    }

    describe('NOOGL.Canvas', function () {
        describe('construction', function () {
            it('adds a canvas tag when appended to an element', function () {
                var gl, div;

                gl = new NOOGL.Canvas();
                div = document.createElement('div');

                gl.appendTo(div);

                expect(div.children.length, 'child count').to.equal(1);
                expect(div.children[0].tagName.toLowerCase(), 'element tag name').to.equal('canvas');
            });

            it('can be moved from one element to another', function () {
                var gl, div1, div2;

                gl = new NOOGL.Canvas();
                div1 = document.createElement('div');
                div2 = document.createElement('div');

                gl.appendTo(div1);
                expect(div1.children.length, 'div1 child count').to.equal(1);
                expect(div2.children.length, 'div2 child count').to.equal(0);

                gl.appendTo(div2);
                expect(div1.children.length, 'div1 child count').to.equal(0);
                expect(div2.children.length, 'div2 child count').to.equal(1);
            });

            function expectGLParameter(gl, param) {
                var div, context, value;

                div = document.createElement('div');
                gl.appendTo(div);
                context = div.children[0].getContext('experimental-webgl');
                value = context.getParameter(context[param]);

                return expect(Array.prototype.slice.call(value), 'GL Parameter "' + param + '"');
            }

            it('has default option values', function () {
                var gl = new NOOGL.Canvas();

                expectGLParameter(gl, 'COLOR_CLEAR_VALUE').to.equal([0, 0, 0, 1]);
                expectGLParameter(gl, 'VIEWPORT').to.equal([0, 0, 300, 150]);
            });

            it('can be constructed with custom dimensions', function () {
                var gl = new NOOGL.Canvas({width: 500, height: 250});

                expectGLParameter(gl, 'VIEWPORT').to.equal([0, 0, 500, 250]);
            });
        });

        describe('as a factory object', function () {
            var gl = new NOOGL.Canvas();

            it('has factory methods', function () {
                expect(gl, gl.constructor.name).to.haveProperty('createProgram');
                expect(gl, gl.constructor.name).to.haveProperty('createShaderFromSource');
                expect(gl, gl.constructor.name).to.haveProperty('createShaderFromScript');
            });

            describe('its shader (from script) factory method', function () {
                it('throws an exception when not passed a script element', function () {
                    expect(gl.createShaderFromScript).with(gl).to.throw('invalid argument');
                });

                it('throws an exception when script does not have a type', function () {
                    var script = document.createElement('script');
                    expect(gl.createShaderFromScript).with(gl, script).to.throw('"" is not a valid shader type');
                });

                it('throws an exception when script has wrong type', function () {
                    var script = document.createElement('script');
                    script.type = 'foo';

                    expect(gl.createShaderFromScript).with(gl, script).to.throw('"foo" is not a valid shader type');
                });

                it('creates a fragment shader from script', function () {
                    var script, shader;

                    script = document.createElement('script');
                    script.type = 'x-glsl/x-fragment';
                    script.textContent = 'void main(){}';

                    shader = gl.createShaderFromScript(script);

                    expect(shader, 'shader').to.be.an('object');
                    expect(shader.type()).to.equal(window.WebGLRenderingContext.FRAGMENT_SHADER);
                });

                it('creates a vertex shader from script', function () {
                    var script, shader;

                    script = document.createElement('script');
                    script.type = 'x-glsl/x-vertex';
                    script.textContent = 'void main(){}';
                    shader = gl.createShaderFromScript(script);

                    expect(shader, 'shader').to.be.an('object');
                    expect(shader.type()).to.equal(window.WebGLRenderingContext.VERTEX_SHADER);
                });
            });

            describe('its shader (from source) factory method', function () {
                it('throws an exception when given a bad type argument', function () {
                    expect(gl.createShaderFromSource).with(gl).to.throw('undefined is not a valid shader type');
                    expect(gl.createShaderFromSource).with(gl, '').to.throw('"" is not a valid shader type');
                    expect(gl.createShaderFromSource).with(gl, 'foo').to.throw('"foo" is not a valid shader type');
                });

                it('throws an exception when good type and no source argument', function () {
                    expect(gl.createShaderFromSource).with(gl, 'x-glsl/x-vertex').to.throw(/syntax error/);
                    expect(gl.createShaderFromSource).with(gl, 'x-glsl/x-fragment').to.throw(/syntax error/);
                });

                it('throws an exception when there is a compiler error', function () {
                    expect(gl.createShaderFromSource).with(gl, 'x-glsl/x-fragment', 'syntax error').to.throw(/syntax error/);
                });

                it('creates a shader from source', function () {
                    expect(gl.createShaderFromSource('x-glsl/x-fragment', 'void main(){}').type()).to.equal(window.WebGLRenderingContext.FRAGMENT_SHADER);
                    expect(gl.createShaderFromSource('x-glsl/x-vertex', 'void main(){}').type()).to.equal(window.WebGLRenderingContext.VERTEX_SHADER);
                });
            });

            describe('its program factory method', function () {
                var vs, fs;

                vs = gl.createShaderFromSource('x-glsl/x-vertex', 'void main(){}');
                fs = gl.createShaderFromSource('x-glsl/x-fragment', 'void main(){}');

                it('throws an exception when an argument is not a shader', function () {
                    expect(gl.createProgram).with(gl, {}).to.throw('argument not a shader');
                    expect(gl.createProgram).with(gl, vs, {}).to.throw('argument not a shader');
                });

                it('throws an exception when there is a linker error', function () {
                    var match = BROWSER.correctlyCapturesProgramInfoLog() ? /missing .*shader/i : '';
                    expect(gl.createProgram).with(gl).to.throw(match);
                    expect(gl.createProgram).with(gl, vs).to.throw(match);
                    expect(gl.createProgram).with(gl, fs).to.throw(match);
                });

                it('creates a shader program without attrubutes', function () {
                    expect(gl.createProgram(vs, fs).getAttributes()).to.equal([]);
                });

                it('creates a shader program with inactive attrubutes', function () {
                    var alt_vs = gl.createShaderFromSource('x-glsl/x-vertex', 'attribute vec4 a; void main(){}');
                    expect(gl.createProgram(alt_vs, fs).getAttributes()).to.equal([]);
                });

                it('creates a shader program with active attrubutes', function () {
                    var alt_vs = gl.createShaderFromSource('x-glsl/x-vertex', 'attribute vec4 a; void main(){ gl_Position = a;}');
                    expect(gl.createProgram(alt_vs, fs).getAttributes()).to.equal(['a']);
                });
            });
        });
    });

    describe('NOOGL.Scene', function () {
        describe('its render function', function () {
            function CallMonitor() {
                var i, args = Array.prototype.slice.apply(arguments);

                this.calls = {};
                for (i = 0; i < args.length; i += 1) {
                    this.monitor(args[i]);
                }
            }

            CallMonitor.prototype.monitor = function (fn) {
                this.calls[fn] = [];
                this[fn] = function () {
                    this.calls[fn].push(arguments);
                };
            };

            it('clears the canvas', function () {
                var s, gl;
                s = new NOOGL.Scene();
                gl = new CallMonitor('clear');
                s.render(gl);
                expect(gl.calls.clear).to.equal([[]]);
            });

            it('renders object that was added to it', function () {
                var s, gl, o;
                s = new NOOGL.Scene();
                gl = new CallMonitor('clear');
                o = new CallMonitor('render');
                s.add(o);
                s.render(gl);
                expect(o.calls.render).to.equal([[gl]]);
            });

            it('renders objects that were added to it', function () {
                var s, gl, o1, o2;
                s = new NOOGL.Scene();
                gl = new CallMonitor('clear');
                o1 = new CallMonitor('render');
                o2 = new CallMonitor('render');
                s.add(o1);
                s.add(o2);
                s.render(gl);
                expect(o1.calls.render).to.equal([[gl]]);
                expect(o2.calls.render).to.equal([[gl]]);
            });

            it('renders objects that were added (multiple per add call) to it', function () {
                var s, gl, o;
                s = new NOOGL.Scene();
                gl = new CallMonitor('clear');
                o = new CallMonitor('render');
                s.add(o, o);
                s.render(gl);
                expect(o.calls.render).to.equal([[gl], [gl]]);
            });
        });
    });

    describe('orderedTriple comparisson function', function () {
        describe('with numbers', function () {
            it('expects non-triples to not equal', function () {
                expect([1, 2, 3]).withCmp(orderedTriple).to.not.equal([1, 2]);
                expect([1, 2]).withCmp(orderedTriple).to.not.equal([1, 2, 3]);
                expect([1, 2, 3]).withCmp(orderedTriple).to.not.equal([1, 2, 3, 4]);
                expect([1, 2, 3, 4]).withCmp(orderedTriple).to.not.equal([1, 2, 3]);
            });

            it('expects differing triples to not equal', function () {
                expect([1, 2, 3]).withCmp(orderedTriple).to.not.equal([1, 2, 4]);
                expect([1, 2, 3]).withCmp(orderedTriple).to.not.equal([1, 4, 3]);
                expect([1, 2, 3]).withCmp(orderedTriple).to.not.equal([4, 2, 3]);
            });

            it('expects permuted triples to equal', function () {
                expect([1, 2, 3]).withCmp(orderedTriple).to.equal([1, 2, 3]);
                expect([1, 2, 3]).withCmp(orderedTriple).to.equal([2, 3, 1]);
                expect([1, 2, 3]).withCmp(orderedTriple).to.equal([3, 1, 2]);
            });

            it('expects non-permuted triples with equal elements to not equal', function () {
                expect([1, 2, 3]).withCmp(orderedTriple).to.not.equal([1, 3, 2]);
                expect([1, 2, 3]).withCmp(orderedTriple).to.not.equal([2, 1, 3]);
                expect([1, 2, 3]).withCmp(orderedTriple).to.not.equal([3, 2, 1]);
            });
        });

        describe('with objects', function () {
            it('expects identical triples to equal', function () {
                expect([[1], [2], [3]]).withCmp(orderedTriple).to.equal([[1], [2], [3]]);
                expect([[1], [2], [3]]).withCmp(orderedTriple).to.equal([[2], [3], [1]]);
                expect([[1], [2], [3]]).withCmp(orderedTriple).to.equal([[3], [1], [2]]);
            });

            it('expects non-permuted triples with equal elements to not equal', function () {
                expect([[1], [2], [3]]).withCmp(orderedTriple).to.not.equal([[1], [3], [2]]);
                expect([[1], [2], [3]]).withCmp(orderedTriple).to.not.equal([[2], [1], [3]]);
                expect([[1], [2], [3]]).withCmp(orderedTriple).to.not.equal([[3], [2], [1]]);
            });
        });

    });

    describe('Array.flatten', function () {
        it('flattens an array of numbers', function () {
            expect([].flatten()).to.equal([]);
            expect([1].flatten()).to.equal([1]);
            expect([1, 2].flatten()).to.equal([1, 2]);
            expect([1, 2, 3].flatten()).to.equal([1, 2, 3]);
        });
        it('flattens nested arrays', function () {
            expect([1, [2]].flatten()).to.equal([1, 2]);
            expect([1, [2, [3]]].flatten()).to.equal([1, 2, 3]);
            expect([1, [2, [3, [4]]]].flatten()).to.equal([1, 2, 3, 4]);
        });
    });

    describe('smoke', function () {
        describe('vertex shader', function () {
            it('has an active position attribute', function () {
                var gl, fs, vs, p;
                gl = new NOOGL.Canvas();
                fs = gl.createShaderFromScript(document.getElementById('fragment-shader'));
                vs = gl.createShaderFromScript(document.getElementById('vertex-shader'));

                p = gl.createProgram(vs, fs);
                expect(p.getAttributes()).to.equal(['position', 'normal']);
                expect(p.getUniforms()).to.equal(['mvpMatrix']);
            });
        });
    });

    describe('Perspective Camera', function () {
        function PerspectiveCamera(w, h, fl, near, far) {
            var af, an;
            af = far + fl;
            an = near + fl;

            this.p = Matrix.zeros(4);

            this.p.m[0][0] = 2 * fl / w;
            this.p.m[1][2] = 2 * fl / h;
            this.p.m[2][1] = (af + an) / (af - an);
            this.p.m[2][3] = (fl * (an + af) - 2 * af * an) / (af - an);
            this.p.m[3][1] = 1;
            this.p.m[3][3] = fl;
        }

        // inheritance:
        //        function Camera() {
        //        }
        //        PerspectiveCamera.prototype = new Camera();

        PerspectiveCamera.prototype.matrix = function () {
            return this.p;
        };

        function homogeneousVectorEquals(a, b, eql, tolerance) {
            var i, n, wa, wb;

            if (!(a instanceof Array && b instanceof Array)) {
                return false;
            }

            if (a.length !== b.length) {
                return false;
            }

            n = a.length;
            wa = a[n - 1];
            wb = b[n - 1];

            if (wa === 0 && wb === 0) {
                wa = 1;
                wb = 1;
            } else if (wa === 0 || wb === 0) {
                return false;
            }

            for (i = 0; i < n - 1; i += 1) {
                if (!eql(a[i] / wa, b[i] / wb, eql, tolerance)) {
                    return false;
                }
            }

            return true;
        }

        describe('40mm square sensor at the origin', function () {
            describe('with 1/2 width focal length', function () {
                describe('projects the corners of the frustum to the canonical volume', function () {
                    it('from sensor up to 1000mm', function () {
                        var camera = new PerspectiveCamera(40, 40, 20, 0, 1000), P;
                        P = camera.matrix();

                        expect(P.times([-20, 0, -20, 1])).withCmp(homogeneousVectorEquals).to.within(1e-6).equal([-1, -1, -1, 1]);
                        expect(P.times([-20, 0, 20, 1])).withCmp(homogeneousVectorEquals).to.within(1e-6).equal([-1, 1, -1, 1]);
                        expect(P.times([20, 0, -20, 1])).withCmp(homogeneousVectorEquals).to.within(1e-6).equal([1, -1, -1, 1]);
                        expect(P.times([20, 0, 20, 1])).withCmp(homogeneousVectorEquals).to.within(1e-6).equal([1, 1, -1, 1]);

                        expect(P.times([-1020, 1000, -1020, 1])).withCmp(homogeneousVectorEquals).to.within(1e-6).equal([-1, -1, 1, 1]);
                        expect(P.times([-1020, 1000, 1020, 1])).withCmp(homogeneousVectorEquals).to.within(1e-6).equal([-1, 1, 1, 1]);
                        expect(P.times([1020, 1000, -1020, 1])).withCmp(homogeneousVectorEquals).to.within(1e-6).equal([1, -1, 1, 1]);
                        expect(P.times([1020, 1000, 1020, 1])).withCmp(homogeneousVectorEquals).to.within(1e-6).equal([1, 1, 1, 1]);
                    });
                    it('from 1000mm up to 2000mm', function () {
                        var camera = new PerspectiveCamera(40, 40, 20, 1000, 2000), P;
                        P = camera.matrix();

                        expect(P.times([-1020, 1000, -1020, 1])).withCmp(homogeneousVectorEquals).to.within(1e-6).equal([-1, -1, -1, 1]);
                        expect(P.times([-1020, 1000, 1020, 1])).withCmp(homogeneousVectorEquals).to.within(1e-6).equal([-1, 1, -1, 1]);
                        expect(P.times([1020, 1000, -1020, 1])).withCmp(homogeneousVectorEquals).to.within(1e-6).equal([1, -1, -1, 1]);
                        expect(P.times([1020, 1000, 1020, 1])).withCmp(homogeneousVectorEquals).to.within(1e-6).equal([1, 1, -1, 1]);

                        expect(P.times([-2020, 2000, -2020, 1])).withCmp(homogeneousVectorEquals).to.within(1e-6).equal([-1, -1, 1, 1]);
                        expect(P.times([-2020, 2000, 2020, 1])).withCmp(homogeneousVectorEquals).to.within(1e-6).equal([-1, 1, 1, 1]);
                        expect(P.times([2020, 2000, -2020, 1])).withCmp(homogeneousVectorEquals).to.within(1e-6).equal([1, -1, 1, 1]);
                        expect(P.times([2020, 2000, 2020, 1])).withCmp(homogeneousVectorEquals).to.within(1e-6).equal([1, 1, 1, 1]);
                    });
                });
            });
        });
    });
}());
